/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator;

import java.util.ArrayList;
import java.util.List;

import maxbe.rel2xml.generator.docs.DataType;
import maxbe.rel2xml.generator.docs.constraints.Constants;
import maxbe.rel2xml.generator.docs.constraints.DataConstraint;
import maxbe.rel2xml.generator.docs.constraints.DerivedValues;
import maxbe.rel2xml.generator.docs.constraints.Nulls;
import maxbe.rel2xml.generator.docs.constraints.PartialConstraint;
import maxbe.rel2xml.generator.docs.constraints.References;
import maxbe.rel2xml.generator.docs.constraints.UniqueNumbers;
import maxbe.rel2xml.generator.docs.constraints.ValueConstructor;
import maxbe.rel2xml.grammar.FillTableException;
import maxbe.rel2xml.util.Config;

import org.apache.log4j.Logger;

/**
 * Represents a column in a relational table with information for generating
 * values
 * 
 * @author Max Bechtold
 * 
 */
public class Column {

	/**
	 * Defines whether a column is to be represented as an XML element or
	 * attribute
	 */
	public static enum Mapping {
		XML_ELEMENT,

		XML_ATTRIBUTE
	}

	private static Logger log = Logger.getLogger(Column.class);

	/** The name of this column */
	private final String name;

	/** Data type and length (if applicable) of this column */
	private final DataType dataType;
	private final int length;
	private final int subLength;

	/** Some conceptional properties */
	private boolean primaryKey;
	private boolean unique;
	private boolean notNull;

	/** Table this column belongs to */
	private Table table;

	/**
	 * Identifies the composite primary or unique key this column is part of, <0
	 * means single key, >0 means composite key, 0 means no key
	 */
	private int compositeKeyId = 0;

	/** Identifies the composite foreign key this column is part of */
	private int compositeForeignKeyId = 0;

	/**
	 * Column this column is either defined on as a foreign key, or is derived
	 * from
	 */
	private Column refColumn;

	/** Stated whether this columns values are derived from another column */
	private boolean derived;

	/**
	 * Field stating whether this column is implicit through the table hierarchy
	 * and thus can be omitted
	 */
	private boolean implicit;

	/**
	 * Defines the number of effectless mark method calls, used for tables with
	 * multiple dependencies or partial, reference, and unique contexts
	 */
	private int markInterval = 1;
	private int markCounter = 1;

	/** List of pregenerated values */
	private List<String> prepValues;
	private int prepIndex = 0;

	/** The DataConstraint of this column */
	private DataConstraint dataConstraint;

	/** Describes how this column is represented in XML */
	private Mapping mapping;

	public Column(String name, DataType dataType, Integer length1,
			Integer length2) {
		this.name = name;
		this.dataType = dataType;
		this.length = (length1 != null ? length1.intValue() : -1);
		this.subLength = (length2 != null ? length2.intValue() : -1);

		// Set default mapping
		if (Config.getBoolean(Config.MAP_ATTRIBUTES)) {
			mapping = Mapping.XML_ATTRIBUTE;
		} else if (Config.getBoolean(Config.MAP_ELEMENTS)) {
			mapping = Mapping.XML_ELEMENT;
		} else if (dataType == DataType.INTEGER) {
			mapping = Mapping.XML_ATTRIBUTE;
		} else {
			mapping = Mapping.XML_ELEMENT;
		}
	}

	public String getName() {
		return name;
	}

	public String getXmlName() {
		return (mapping == Mapping.XML_ATTRIBUTE ? "@" : "") + name;
	}

	public DataType getDataType() {
		return dataType;
	}

	public int getLength() {
		return length;
	}

	public int getSubLength() {
		return subLength;
	}

	public int getMarkInterval() {
		return markInterval;
	}

	public DataConstraint getDataConstraint() {
		return dataConstraint;
	}

	public Mapping getMapping() {
		return mapping;
	}

	public Table getTable() {
		return table;
	}

	public Column getRefColumn() {
		return refColumn;
	}

	public int getCompositeKeyId() {
		return compositeKeyId;
	}

	public int getCompositeForeignKeyId() {
		return compositeForeignKeyId;
	}

	public boolean isPrimaryKey() {
		return primaryKey;
	}

	public boolean isUnique() {
		return unique;
	}

	public boolean isNotNull() {
		return notNull;
	}

	public boolean isImplicit() {
		return implicit;
	}

	public boolean isDerived() {
		return derived;
	}

	public void setPrimaryKey(boolean primaryKey) {
		this.primaryKey = primaryKey;
		unique = true;
		notNull = true;
	}

	public void setUnique(boolean unique) {
		this.unique = unique;
	}

	public void setNotNull(boolean notNull) {
		this.notNull = notNull;
	}

	public void setImplicit(boolean implicit) {
		this.implicit = implicit;
	}

	public void setDerived(boolean derived) {
		this.derived = derived;
	}

	public void setTable(Table table) {
		this.table = table;
	}

	public void setMarkInterval(int markInterval) {
		this.markInterval = markInterval;
	}

	public void setRefColumn(Column refColumn) {
		this.refColumn = refColumn;
	}

	public void setRefValues(List<String> refValues) {
		References ref = (References) dataConstraint.getValueConstructor();
		if (table.getFillFactor().varies()) {
			int[] factors = table.getFactors();
			if (ref.isDependent())
				// Multiply factors by number of contexts (calls of mark method)
				// per refColumn.table tuple
				for (int i = 0; i < factors.length; i++)
					factors[i] *= table.getBase()
							/ refColumn.table.getCardinality();

			ref.setRefValues(refValues, factors);
		} else {
			// Multiply factors by number of contexts (calls of mark method) per
			// refColumn.table tuple
			int factor = table.getFactor();
			if (ref.isDependent())
				factor *= (table.getBase() / refColumn.table.getCardinality());

			ref.setRefValues(refValues, factor);
		}
	}

	public void setCompositeKeyId(int compositeKeyId) {
		this.compositeKeyId = compositeKeyId;
	}

	public void setCompositeForeignKeyId(int compositeForeignKeyId) {
		this.compositeForeignKeyId = compositeForeignKeyId;
	}

	public void setDataConstraint(DataConstraint dataConstraint) {
		ValueConstructor vc = dataConstraint.getValueConstructor();
		PartialConstraint pcOnFirst = dataConstraint.getPcOnFirst();
		PartialConstraint pcOnLast = dataConstraint.getPcOnLast();
		PartialConstraint pcOnRandom = dataConstraint.getPcOnRandom();

		if (notNull
				&& (vc instanceof Nulls
						|| (pcOnFirst != null && pcOnFirst
								.getValueConstructor() instanceof Nulls)
						|| (pcOnLast != null && pcOnLast.getValueConstructor() instanceof Nulls) || (pcOnRandom != null && pcOnRandom
						.getValueConstructor() instanceof Nulls))) {
			String msg = String
					.format("Column '%s.%s' is defined as not null and therefore cannot have Nulls as DataConstraint or PartialConstraint.",
							table.getName(), name);
			log.error(msg);
			throw (new FillTableException(msg));
		}

		if (unique
				&& (pcOnFirst != null || pcOnLast != null || pcOnLast != null || !((vc instanceof UniqueNumbers && (table
						.getDependencies().size() == 0 || ((UniqueNumbers) vc)
						.isGlobal())) || (vc instanceof References && ((References) vc)
						.isUnique())))) {
			String msg = String
					.format("Column '%s.%s' is defined as unique and therefore must have a global UniqueNumbers or a unique References as ValueConstructor without any PartialConstraints.",
							table.getName(), name);
			log.error(msg);
			throw (new FillTableException(msg));
		}

		// Whether PartialConstraints can derive their boundary values
		// themselves or they get it set
		if (pcOnFirst != null && !pcOnFirst.isGlobal() && markInterval > 1
				&& table.getFillFactor().varies())
			pcOnFirst.setDeriveBoundary(false);

		if (pcOnLast != null && !pcOnLast.isGlobal() && markInterval > 1
				&& table.getFillFactor().varies())
			pcOnLast.setDeriveBoundary(false);

		if (vc instanceof DerivedValues) {
			DerivedValues dv = (DerivedValues) vc;
			Column sourceColumn = dv.getSourceColumn();

			if (this.dataType != sourceColumn.dataType
					|| this.length != sourceColumn.length
					|| this.subLength != sourceColumn.subLength) {
				String msg = String
						.format("Column '%s.%s' cannot be derived from '%s.%s' as their datatype, length, and/or subLengh are not equal.",
								table.getName(), this.name, sourceColumn
										.getTable().getName(),
								sourceColumn.name);
				log.error(msg);
				throw (new FillTableException(msg));
			}

			// Check dependency relationship later as the other tables might not
			// be initialized already

			derived = true;

			if (sourceColumn.getTable() == this.table) {
				dv.setLocal(true);
			}

			refColumn = sourceColumn;
		}

		if (refColumn != null && !derived) {
			if (!(vc instanceof References)) {
				String msg = String
						.format("Column '%s.%s' is defined as a foreign key and therefore must have References as ValueConstructor of DataConstraint.",
								table.getName(), name);
				log.error(msg);
				throw (new FillTableException(msg));
			}

			References reference = (References) vc;

			if (table.getDependencies().contains(refColumn.getTable())
					&& reference.isUnique()) {
				String msg = String
						.format("The table of column '%s.%s' depends on the table the foreign key refers to and therefore cannot be specified as a unique reference.",
								table.getName(), name);
				log.error(msg);
				throw (new FillTableException(msg));
			}

			if (reference.isDependent()) {
				if (!table.dependsOn(refColumn.getTable(), false, true)) {
					String msg = String
							.format("The table of column '%s.%s' does not depend primarily on the table '%s' it references, thus '%s' cannot be a dependent reference.",
									table.getName(), name, refColumn.getTable()
											.getName(), name);
					log.error(msg);
					throw (new FillTableException(msg));
				}

				if (!notNull) {
					String msg = String
							.format("Column '%s.%s' is not specified as not null and therefore cannot be a dependent reference.",
									table.getName(), name);
					log.error(msg);
					throw (new FillTableException(msg));
				}
			}

			if (compositeForeignKeyId > 0) {
				for (Column column : table.getRefColumns()) {
					DataConstraint dc = column.getDataConstraint();
					if (column.getCompositeForeignKeyId() == compositeForeignKeyId
							&& dc != null) {
						References otherRef = ((References) dc
								.getValueConstructor());
						if (reference.isDependent() != otherRef.isDependent()
								|| reference.isRandom() != otherRef.isRandom()
								|| reference.isUnique() != otherRef.isUnique()
								|| reference.getRefContext() != otherRef
										.getRefContext()
								|| reference.getStartFrom() != otherRef
										.getStartFrom()) {
							String msg = String
									.format("Column '%s.%s' is part of an composite foreign key and specify the same properties (unique, random, dependent, refContext, startFrom) as them.",
											table.getName(), name);
							log.error(msg);
							throw (new FillTableException(msg));
						}
					}
				}
			}

			if (compositeForeignKeyId > 0) {
				reference.setCompositeForeignKeyId(compositeForeignKeyId);
			}

		} else if (!derived) {
			// Type and length check are performed when setting this column's
			// refColumn
			// -> no need to do the following for References
			if (!(vc instanceof Constants))
				checkTypeAndLength(vc);
			if (pcOnFirst != null
					&& !(pcOnFirst.getValueConstructor() instanceof Constants))
				checkTypeAndLength(pcOnFirst.getValueConstructor());
			if (pcOnRandom != null
					&& !(pcOnRandom.getValueConstructor() instanceof Constants))
				checkTypeAndLength(pcOnRandom.getValueConstructor());
			if (pcOnLast != null
					&& !(pcOnLast.getValueConstructor() instanceof Constants))
				checkTypeAndLength(pcOnLast.getValueConstructor());
		}

		this.dataConstraint = dataConstraint;
	}

	private void checkTypeAndLength(ValueConstructor vc) {
		if (!vc.getSupportedDataTypes().contains(dataType)) {
			String msg = String
					.format("The ValueConstructor '%s' does not support the dataType '%s' of column '%s'.",
							vc.getClass().getSimpleName(), dataType, name);
			log.error(msg);
			throw (new FillTableException(msg));
		}

		if ((dataType.lengthVaries() && (length < vc.getLength() || subLength != vc
				.getSubLength()))
				|| !dataType.lengthVaries()
				&& (vc.lengthVaries() || length != vc.getLength() || subLength != vc
						.getSubLength())) {
			String msg = String
					.format("Length or subLength of ValueConstructor '%s' does not match that of column '%s'.",
							vc, name);
			log.error(msg);
			throw (new FillTableException(msg));
		}
	}

	public void setMapping(Mapping mapping) {
		this.mapping = mapping;
	}

	@Override
	public String toString() {
		String result = name + " " + dataType;
		if (length != -1) {
			result += " (" + length;
			if (subLength != -1) {
				result += ", " + subLength;
			}
			result += ")";
		}
		if (notNull) {
			result += " not null";
		}
		if (unique) {
			result += " unique";
		}
		if (primaryKey) {
			result += " primary key";
		}
		if (refColumn != null && !derived) {
			result += String.format(" references %s(%s)", refColumn.getTable()
					.getName(), refColumn.getName());
		}
		return result;
	}

	public boolean needsAnnounce() {
		return dataConstraint.needsAnnounce();
	}

	public void announce(int number) {
		dataConstraint.announce(number);
	}

	public void mark(int currContext) {
		if (--markCounter == 0) {
			markCounter = markInterval;

			int factor = 0;
			if (table.getFillFactor().varies()) {
				factor = aggregateFactors(currContext);
			} else {
				factor = table.getFactor() * markInterval;
			}

			if (prepValues == null)
				dataConstraint.mark(factor);
		}
	}

	private int aggregateFactors(int index) {
		int factor = 0;
		int[] factors = table.getFactors();
		if (markInterval == 1) {
			factor = factors[index];
		} else {
			// Aggregate factors
			int i = 0;
			PartialConstraint pcOnFirst = dataConstraint.getPcOnFirst();
			if (pcOnFirst != null && !pcOnFirst.isGlobal()) {
				for (; i < pcOnFirst.getPercentage() * markInterval / 100; i++)
					factor += factors[index++];
				pcOnFirst.setBoundary(factor);
			}

			PartialConstraint pcOnLast = dataConstraint.getPcOnLast();
			if (pcOnLast != null && !pcOnLast.isGlobal()) {
				for (; i < markInterval - pcOnLast.getPercentage()
						* markInterval / 100; i++)
					factor += factors[index++];
				int count = factor;
				for (; i < markInterval; i++)
					factor += factors[index++];
				pcOnLast.setBoundary(factor - count);
			} else {
				for (; i < markInterval; i++)
					factor += factors[index++];
			}

		}
		return factor;
	}

	public List<String> next(int number) {
		if (prepValues != null && prepValues.size() > 0) {
			prepIndex += number;
			return prepValues.subList(prepIndex - number, prepIndex);
		} else {
			return dataConstraint.next(number);
		}
	}

	public List<String> prepareValues() {
		if (prepValues == null) {
			List<String> tmp = new ArrayList<String>(table.getCardinality());

			// Let referenced column prepare its values
			if (refColumn != null) {
				List<String> prepVals = refColumn.prepareValues();

				if (derived)
					((DerivedValues) dataConstraint.getValueConstructor())
							.setSourceVals(prepVals);
				else
					setRefValues(prepVals);
			}

			// Prepare own values
			announce(table.getCardinality());

			for (int i = 0; i < table.getBase(); i++) {
				int factor = 0;
				if (table.getFillFactor().varies()) {
					if (table.getFactors() != null) {
						factor = table.getFactors()[i];
					} else {
						factor = table.getFactor();
					}
				} else {
					factor = table.getFactor();
				}
				mark(i);
				tmp.addAll(next(factor));
			}

			// Store generated values appropriately
			prepValues = tmp;
		}

		return prepValues;
	}

}
